package com.perceptivesoftware.licensing.server.util;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.Address;
import javax.mail.AuthenticationFailedException;
import javax.mail.Message;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.net.ssl.SSLHandshakeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.perceptivesoftware.licensing.client.common.exceptions.EmailSendFailureException;
import com.perceptivesoftware.licensing.client.common.exceptions.NeedsTLSException;
import com.perceptivesoftware.licensing.client.common.exceptions.NotSSLException;
import com.perceptivesoftware.licensing.client.common.util.SystemParmType;
import com.perceptivesoftware.licensing.client.dto.EmailDTO;
import com.perceptivesoftware.licensing.client.dto.NotificationDetailDTO;
import com.perceptivesoftware.licensing.client.dto.SystemParmDTO;
import com.sun.mail.smtp.SMTPSSLTransport;
import com.sun.mail.smtp.SMTPSendFailedException;

@Component
public class MailUtil {

	private static final Log logger = LogFactory.getLog(MailUtil.class);

	@Autowired
	private SystemServiceUtil systemUtil;

	private static final HashMap<String, String> serverSecurity = new HashMap<String, String>();

	public void sendEmail(final EmailDTO email) throws EmailSendFailureException {
		final EmailDTO emailDto = getSystemEmailInfo(email);
		InternetAddress recipients[] = null;
		InternetAddress ccRecipients[] = null;
		final List<String> toAddrs = new ArrayList<String>();
		for (final NotificationDetailDTO detail : emailDto.getNotificationGroup().getToGroup()) {
			if (detail.getUser() != null) {
				toAddrs.add(detail.getUser().getEmail());
			}
			else {
				toAddrs.add(detail.getEmail());
			}
		}
		final List<String> CCs = new ArrayList<String>();
		for (final NotificationDetailDTO detail : emailDto.getNotificationGroup().getCcGroup()) {
			if (detail.getUser() != null) {
				CCs.add(detail.getUser().getEmail());
			}
			else {
				CCs.add(detail.getEmail());
			}
		}
		recipients = new InternetAddress[toAddrs.size()];
		for (int i = 0; i < toAddrs.size(); i++) {
			try {
				recipients[i] = new InternetAddress(toAddrs.get(i));
			}
			catch (final AddressException e) {
				logger.warn("Invalid to email address: " + toAddrs.get(i));
			}
		}
		ccRecipients = new InternetAddress[CCs.size()];
		for (int i = 0; i < CCs.size(); i++) {
			try {
				ccRecipients[i] = new InternetAddress(CCs.get(i));
			}
			catch (final AddressException e) {
				logger.warn("Invalid to email address: " + CCs.get(i));
			}
		}
		final String hostColonPort = emailDto.getHost() + ":" + emailDto.getPort();
		final String security = serverSecurity.get(hostColonPort);
		if (security != null) // we have hit this server before, figure out what
								// security it had
		{
			if (security.equals("TLS")) {
				logger.debug("recognized " + hostColonPort + " as TLS");
				sendTLSMessage(emailDto, recipients, ccRecipients);
				return;
			}
			else if (security.equals("Plain")) {
				logger.debug("recognized " + hostColonPort + " as Plaintext");
				try {
					sendPlaintextMessage(emailDto, recipients, ccRecipients);
					return;
				}
				catch (final NeedsTLSException e) {
					// fall through to below check-all as it is not actually tls
				}
			}
			else if (security.equals("SSL")) {
				try {
					logger.debug("recognized " + hostColonPort + " as SSL");
					sendSSLMessage(emailDto, recipients, ccRecipients);
					return;
				}
				catch (final NotSSLException e) {
					// fall through to below check-all as it is not actually ssl
				}
				catch (final NeedsTLSException e) {
					// fall through to below check-all as it needs TLS
				}
			}
		}
		try {
			sendSSLMessage(emailDto, recipients, ccRecipients);
			serverSecurity.put(hostColonPort, "SSL");
			logger.debug("Saving " + hostColonPort + " as SSL");
			return;
		}
		catch (final NotSSLException nse) {
			try {
				sendPlaintextMessage(emailDto, recipients, ccRecipients);
				serverSecurity.put(hostColonPort, "Plain");
				logger.debug("Saving " + hostColonPort + " as Plain");
				return;
			}
			catch (final NeedsTLSException e1) {
				// fall through to TLS
			}
		}
		catch (final NeedsTLSException e) {
			// fall through to TLS
		}
		sendTLSMessage(emailDto, recipients, ccRecipients);
		serverSecurity.put(hostColonPort, "TLS");
		logger.debug("Saving " + hostColonPort + " as TLS");
	}

	public EmailDTO getSystemEmailInfo(EmailDTO email) throws EmailSendFailureException {
		final SystemParmDTO parmDto = systemUtil.getSystemParmById(SystemParmType.EMAIL_HOST);
		if (parmDto != null) {
			String fields = parmDto.getParmVal();
			int next = fields.indexOf("|");
			String nextField;

			for (int x = 1; x <= 5; x++) {
				if (next == 0) {// empty field
					nextField = "";
				}
				else if (next == -1) {
					nextField = fields;
					if (x == 5) {
						email.setSmtpPass(nextField);
					}
				}
				else {
					nextField = fields.substring(0, next);

					switch (x) {
					case 1:
						email.setFromEmail(nextField);
						break;
					case 2:
						email.setHost(nextField);
						break;
					case 3:
						email.setPort(new Long(nextField));
						break;
					case 4:
						email.setSmtpUser(nextField);
						break;
					case 5:
						email.setSmtpPass(nextField);
						break;
					}
				}

				if (next == -1) {// no more fields
					break;
				}
				else if (next < fields.length()) {
					fields = fields.substring(next + 1);
					next = fields.indexOf("|");
				}
			}

			return email;
		}

		throw new EmailSendFailureException();
	}

	private void configureMessage(Message message, EmailDTO email, Address[] recipients, Address[] ccRecipients) throws EmailSendFailureException {
		try {
			final String fromAddr = email.getFromEmail();
			final String fromName = email.getFromEmail();
			try {
				message.setFrom(new InternetAddress(fromAddr, fromName));
			}
			catch (final UnsupportedEncodingException e) {
				message.setFrom(new InternetAddress(fromAddr));
				message.addRecipient(RecipientType.CC, new InternetAddress(fromAddr));
			}
			for (final Address recip : recipients) {
				message.addRecipient(RecipientType.TO, recip);
			}
			for (final Address ccRecip : ccRecipients) {
				message.addRecipient(RecipientType.CC, ccRecip);
			}
			if (email.getAttachments() != null) {
				final MimeBodyPart messageBodyPart = new MimeBodyPart();
				messageBodyPart.setText(email.getMsg());
				final MimeMultipart multipart = new MimeMultipart();
				multipart.addBodyPart(messageBodyPart);
				MimeBodyPart attachBodyPart;
				DataSource source;
				File attach;
				String attachPath;
				for (int i = 0; i < email.getAttachments().size(); i++) {
					attachPath = email.getAttachments().get(i);
					attach = new File(attachPath);
					attachBodyPart = new MimeBodyPart();
					source = new FileDataSource(attach);
					attachBodyPart.setDataHandler(new DataHandler(source));
					final String fileName = email.getAttachmentNames().get(i);
					attachBodyPart.setFileName(fileName);
					multipart.addBodyPart(attachBodyPart);
				}
				message.setContent(multipart);
			}
			else {
				message.setContent(email.getMsg(), "text/plain");
			}
			message.setSubject(email.getSubject());
		}
		catch (final MessagingException me) {
			logger.error("MessagingException creating email to send. ", me);
			throw new EmailSendFailureException();
		}
	}

	private void sendTLSMessage(EmailDTO email, Address[] recipients, Address[] ccRecipients) throws EmailSendFailureException {
		final Properties mailProperties = new Properties();
		mailProperties.setProperty("mail.smtp.starttls.enable", "true");
		mailProperties.setProperty("mail.host", email.getHost());
		mailProperties.setProperty("mail.port", email.getPort().toString());
		mailProperties.setProperty("mail.transport.protocol", "smtp");
		if (email.getSmtpUser() != null && email.getSmtpUser().length() > 0) {
			mailProperties.setProperty("mail.user", email.getSmtpUser());
			mailProperties.setProperty("mail.password", email.getSmtpPass());
		}
		final Session session = Session.getInstance(mailProperties, null);
		final Message message = new MimeMessage(session);
		configureMessage(message, email, recipients, ccRecipients);
		try {
			final Transport t = session.getTransport("smtp");
			t.connect(email.getHost(), email.getPort().intValue(), email.getSmtpUser(), email.getSmtpPass());
			t.sendMessage(message, message.getAllRecipients());
			t.close();
		}
		catch (final MessagingException e) {
			if (e instanceof AuthenticationFailedException) {
				logger.error("Authentication failed during sendTLSMessage", e);
				throw new EmailSendFailureException();
			}
			else {
				logger.error("MessagingException during sendTLSMessage", e);
				throw new EmailSendFailureException();
			}
		}

	}

	private void sendPlaintextMessage(EmailDTO email, Address[] recipients, Address[] ccRecipients) throws NeedsTLSException, EmailSendFailureException {
		try {
			final Properties mailProperties = new Properties();
			mailProperties.setProperty("mail.smtp.starttls.enable", "false");
			mailProperties.setProperty("mail.host", email.getHost());
			if (email.getSmtpUser() != null && email.getSmtpUser().length() > 0) {
				mailProperties.setProperty("mail.user", email.getSmtpUser());
				mailProperties.setProperty("mail.password", email.getSmtpPass());
			}
			final Session session = Session.getInstance(mailProperties, null);
			final Message message = new MimeMessage(session);
			configureMessage(message, email, recipients, ccRecipients);
			final Transport t = session.getTransport("smtp");
			t.connect(email.getHost(), email.getPort().intValue(), email.getSmtpUser(), email.getSmtpPass());
			t.sendMessage(message, message.getAllRecipients());
			t.close();
		}
		catch (final SMTPSendFailedException ex) {
			if (ex.getMessage().toLowerCase().indexOf("starttls") > 0) {
				throw new NeedsTLSException();
			}
			else {
				logger.error("SMTPSendFailedException during sendPlaintextEmail", ex);
				throw new EmailSendFailureException();
			}
		}
		catch (final MessagingException e) {
			if (e instanceof AuthenticationFailedException) {
				if (e.getMessage().contains("No authentication mechansims supported by both server and client")) {
					throw new NeedsTLSException();
				}
				logger.error("Authentication failed during sendPlaintextEmail", e);
				throw new EmailSendFailureException();
			}
			else {
				logger.error("MessagingException during sendPlaintextEmail", e);
				throw new EmailSendFailureException();
			}
		}
	}

	private void sendSSLMessage(EmailDTO email, Address[] recipients, Address[] ccRecipients) throws NotSSLException, EmailSendFailureException,
			NeedsTLSException {
		try {
			final Properties mailProperties = new Properties();
			mailProperties.setProperty("mail.smtp.starttls.enable", "false");
			mailProperties.setProperty("mail.host", email.getHost());
			if (email.getSmtpUser() != null && email.getSmtpUser().length() > 0) {
				mailProperties.setProperty("mail.user", email.getSmtpUser());
				mailProperties.setProperty("mail.password", email.getSmtpPass());
			}

			final Session session = Session.getInstance(mailProperties, null);
			final Message message = new MimeMessage(session);
			configureMessage(message, email, recipients, ccRecipients);
			final SMTPSSLTransport sslt = (SMTPSSLTransport) session.getTransport("smtps");
			sslt.connect(email.getHost(), email.getPort().intValue(), email.getSmtpUser(), email.getSmtpPass());
			sslt.sendMessage(message, message.getAllRecipients());
			sslt.close();
		}
		catch (final MessagingException me) {
			if (me.getMessage().indexOf("Unrecognized SSL message") > -1
					|| (me.getNextException() != null && me.getNextException().getMessage().indexOf("Unrecognized SSL message") > -1)) {
				throw new NotSSLException();
			}
			else if (me.getMessage().indexOf("STARTTLS") > -1 || (me.getNextException() != null && me.getNextException().getMessage().indexOf("STARTTLS") > -1)) {
				throw new NeedsTLSException();
			}
			else if (me.getNextException() != null && me.getNextException() instanceof SSLHandshakeException) {
				logger.error("SSL Handshake failed, invalid certificate?", me);
				throw new EmailSendFailureException();
			}
			else if (me instanceof AuthenticationFailedException) {
				logger.error("Authentication failed sending email");
				throw new EmailSendFailureException();
			}
			else {
				logger.error("MessagingException during sendSSLMessage", me);
				throw new EmailSendFailureException();
			}
		}
	}
}
